Skip to content

fix: preserve AI attribution when human edits file after AI checkpoint (fixes #1444)#1667

Open
IanMcNelly wants to merge 3 commits into
git-ai-project:mainfrom
IanMcNelly:main
Open

fix: preserve AI attribution when human edits file after AI checkpoint (fixes #1444)#1667
IanMcNelly wants to merge 3 commits into
git-ai-project:mainfrom
IanMcNelly:main

Conversation

@IanMcNelly

Copy link
Copy Markdown

Problem

When a human modifies a file after an AI checkpoint fires but before committing — without a known_human checkpoint being triggered — AI attribution for the AI's own lines was lost (shown as 100% untracked).
Reported in #1444.

Root cause

The carryover snapshot stays stale (holds the AI checkpoint version). An overlap filter in the attribution loop was then stripping the human-modified position from unstaged_hunks because it also appeared in
committed_hunks — incorrectly crediting that committed line to AI. For the modify case (human replaces an AI-written line), this meant the human's content was attributed to AI.

Fix

virtual_attribution.rs: Skip the committed-vs-unstaged overlap filter when a carryover snapshot is present. With a snapshot, Replace-type unstaged lines represent content divergence between the AI
checkpoint and the commit — they should flow to INITIAL, not be silently re-attributed.

attribution_recovery.rs / post_commit.rs: Replace the content-based guard in recover_adjacent_edges with a displaced_to_initial_lines set (intersection of initial_attributions and
recovery_hunks). This blocks edge recovery from re-claiming lines explicitly sent to INITIAL, while preserving edge recovery for genuinely adjacent uncheckpointed inserts/appends.

Tests

Added tests/integration/human_edit_after_ai_checkpoint.rs with four regression tests covering:

  • Human appends lines after AI block (edge recovery attributes adjacent lines to AI — existing product behavior)
  • Human modifies a line within the AI range (the core bug — now correctly unattributed)
  • Human inserts lines before AI block (edge recovery attributes adjacent lines to AI)
  • Partial staging guard: real uncommitted lines still defer to INITIAL

All existing tests pass including the delayed_checkout/switch_merge_trace_replay tests.

Fixes #1444

@CLAassistant

CLAassistant commented Jun 27, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

Open in Devin Review

Comment thread src/authorship/virtual_attribution.rs Outdated
Comment on lines +400 to +418
let displaced_to_initial_lines: HashMap<String, HashSet<u32>> = initial_attributions
.files
.iter()
.filter_map(|(file, line_attrs)| {
let committed_for_file = recovery_hunks.get(file)?;
let committed_set: HashSet<u32> =
committed_for_file.iter().flat_map(|r| r.expand()).collect();
let displaced: HashSet<u32> = line_attrs
.iter()
.flat_map(|la| la.start_line..=la.end_line)
.filter(|l| committed_set.contains(l))
.collect();
if displaced.is_empty() {
None
} else {
Some((file.clone(), displaced))
}
})
.collect();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 displaced_to_initial_lines assumes carryover and committed coordinates are identical

The displaced_to_initial_lines computation at src/authorship/post_commit.rs:400-418 intersects initial_attributions line numbers (carryover/working-directory coordinates) with recovery_hunks line numbers (committed coordinates). These coordinate spaces differ when there are pure insertions in carryover. However, I traced through the scenarios and found that when pure insertions exist (human appends lines after AI checkpoint), the stale-observed detection at src/authorship/virtual_attribution.rs:2058-2062 fires and sets carryover = committed, eliminating all unstaged hunks and producing empty initial_attributions. For replacements (human modifies a line), there are no pure insertions, so coordinates are identical. The assumption appears safe for the targeted scenarios, but if future changes alter the stale-detection logic, this intersection could silently produce incorrect results.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed safe for the current scenarios.

But, I added a comment documenting the invariant and why it holds:
stale-detection in build_carryover_snapshot fires for all cases where pure insertions would exist (human appends/inserts), replacing the carryover with committed content and emptying initial_attributions for those files. The only case that populates initial_attributions is in-place replacement, which has no coordinate shift. Added a note about what would need to change (coordinate-aware conversion using pure_insertion_hunks) if the stale-detection path is ever altered.

IanMcNelly and others added 2 commits July 7, 2026 09:30
When a human modifies a file after an AI checkpoint fires but before
committing — without triggering a known_human checkpoint — the
carryover snapshot could be stale (holding the AI version). This
caused the overlap filter to incorrectly strip the human-modified
line from unstaged_hunks, leaving it attributed to AI.

Fix: skip the committed-vs-unstaged overlap filter entirely when a
carryover snapshot is present. With a snapshot, Replace-type unstaged
lines represent lines where committed content differs from the AI
checkpoint — they should flow to INITIAL, not be re-attributed.

Also replace the content-based guard in recover_adjacent_edges with
a displaced_to_initial_lines set derived from initial_attributions ∩
recovery_hunks. This prevents edge recovery from re-claiming
human-modified lines that were explicitly sent to INITIAL, while
still allowing edge recovery for adjacent uncheckpointed lines
(inserts/appends) that were never in the attribution loop at all.

Fixes git-ai-project#1444

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…-fill

Pure-insertion unstaged lines (present in carryover but absent from the commit) have no committed counterpart. Computing a committed coordinate for them and inserting it into explicitly_resolved_commit_lines fabricates a position that aliases an unrelated committed line, potentially blocking gap-fill attribution for that line.

Add a binary_search guard so only Replace-type unstaged lines (which have a real committed counterpart) claim a committed-coordinate slot.

Also document the coordinate-space invariant in the displaced_to_initial_lines computation: stale-detection ensures initial_attributions is empty for files with pure insertions, so the direct intersection of carryover and committed coordinates is safe for all currently targeted scenarios.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: All attribution lost (100% untracked) when human edits after AI checkpoint in Claude Code

2 participants